import java.applet.Applet;
import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.MediaTracker;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

////////////////////////////////////////////////////////////////

public class Main extends Applet {
  public static final PrintStream debug = new PrintStream(new OutputStream() {
    public void write(int b) throws IOException {
    }
  });
  public static final PrintStream log = System.err;

  public void init() {
    width_ = getSize().width;
    height_ = getSize().height;
    buffer_image_ = createImage(width_, height_);
    buffer_ = buffer_image_.getGraphics();

    addMouseListener(new MouseAdapter() {
      public void mousePressed(MouseEvent e) {
        game_.ui().push_event(e);
      }
    });
    addMouseMotionListener(new MouseMotionAdapter() {
      public void mouseMoved(MouseEvent e) {
        game_.ui().push_event(e);
      }

      public void mouseDragged(MouseEvent e) {
        game_.ui().push_event(e);
      }
    });

    game_ = new Game(this);
    game_thread_ = new Thread(game_);
    game_thread_.start();

    animation_thread_ = new Thread() {
      public void run() {
        try {
          for (;;) {
            Thread.yield();
            if (Thread.currentThread().isInterrupted())
              throw new InterruptedException("Quitting the game.");

            repaint();
            Thread.currentThread().sleep(30);
          }
        } catch (InterruptedException e) {
        }
        debug.println("Animation thread finished.");
      }
    };
    animation_thread_.start();
  }

  public void destroy() {
    game_thread_.interrupt();
    animation_thread_.interrupt();
    try {
      game_thread_.join();
      animation_thread_.join();
    } catch (InterruptedException e) {
      throw new Error("Problem when quitting the game.", e);
    }
  }

  public void paint(Graphics g) {
    g.clearRect(0, 0, width_, height_);
    game_.ui().paint(new UI.TGraphics(g, game_.ui()));
  }

  public void update(Graphics g) {
    // non-double-buffered version
    // paint(g); //debug

    // double-buffered version
    paint(buffer_);
    g.drawImage(buffer_image_, 0, 0, this);
  }

  private Image buffer_image_;
  private Graphics buffer_;
  private Game game_;
  private Thread game_thread_;
  private Thread animation_thread_;
  private int width_;
  private int height_;
}

// //////////////////////////////////////////////////////////////

class Game implements Runnable {
  public static final int CRITICAL_MULTIPLIER = 3;
  public static final int MAX_INVENTORY_SIZE = 5;

  public Game(Applet main) {
    ui_manager_ = new UI.Manager(main);
    ai_ = new AdvancedAI();

    // insert hard-coded terrain
    Terrain[][] terrain = new Terrain[13][13];
    terrain[terrain.length / 2][terrain[0].length / 2] = Terrain.WALL;
    for (int i = 0; i < terrain.length; ++i)
      for (int j = 0; j < terrain[i].length; ++j) {
        if (Math.random() < ((j - i) * (j - i) - 9) * 0.0003) {
          terrain[i][j] = Terrain.WALL;
          terrain[terrain.length - 1 - i][terrain[i].length - 1 - j] = Terrain.WALL;
        }
        if (terrain[i][j] != Terrain.WALL)
          terrain[i][j] = Math.random() < (j - i) * 0.02 + 0.3 ? Terrain.FOREST
              : Terrain.PLAINS;
      }

    for (int loop_counter = 0; loop_counter < 5; ++loop_counter) {
      for (int i = 0; i < terrain.length; ++i) {
        for (int j = 0; j < terrain[i].length; ++j) {
          int wall_count = 0;
          for (Dir dir : Dir.values()) {
            int ii = i + dir.getV();
            int jj = j + dir.getH();
            if (0 < ii && ii < terrain.length && 0 < jj
                && jj < terrain[ii].length)
              if (terrain[ii][jj] == Terrain.WALL)
                ++wall_count;
          }
          if (wall_count == 1) {
            double probability = (double) (j + i)
                / (terrain.length + terrain[i].length - 6) * 2;
            probability = 1 - (1 - probability) * (1 - probability);
            probability /= loop_counter + 2;
            if (Math.random() < probability)
              terrain[i][j] = Terrain.WALL;
          } else if (wall_count == 4)
            terrain[i][j] = Terrain.WALL;
        }
      }
    }
    for (int i = 0; i < terrain.length; ++i)
      for (int j = 0; j < terrain[i].length; ++j) {
        int x = terrain[i].length / 4;
        int y = terrain.length / 4;
        if (i == y || i == terrain.length - 1 - y || j == x
            || j == terrain[i].length - 1 - x)
          if (terrain[i][j] == Terrain.WALL)
            terrain[i][j] = Terrain.PLAINS;
      }

    // insert hard-coded combatants
    Combatant tommy = new Combatant("Tommy", CClass.MAGE, new Attributes(
        17, 5, 6, 7, 3, 2, 5, 4), WeaponTemplate.FIRE);
    Combatant lindsey = new Combatant("Lindsey", CClass.ARCHER,
        new Attributes(17, 4, 6, 6, 4, 3, 1, 3),
        WeaponTemplate.KILLER_BOW);
    Combatant daniel = new Combatant("Daniel", CClass.MYRMIDON,
        new Attributes(22, 6, 9, 8, 3, 3, 1, 7),
        WeaponTemplate.IRON_SWORD);
    Combatant steven = new Combatant("Steven", CClass.KNIGHT,
        new Attributes(20, 5, 7, 4, 1, 7, 1, 8),
        WeaponTemplate.IRON_LANCE);
    Combatant ben = new Combatant("Ben", CClass.FIGHTER, new Attributes(21,
        7, 7, 5, 2, 1, 1, 9), WeaponTemplate.IRON_AXE);
    Combatant healer = new Combatant("Anonymous", CClass.CLERIC,
        new Attributes(16, 5, 5, 6, 3, 1, 6, 4), null);
    healer.acquire(new Staff(StaffTemplate.HEAL));
    Combatant[] opponents = new Combatant[5];
    for (int i = 0; i < opponents.length; ++i) {
      CClass cclass;
      WeaponTemplate template;
      for (;;) {
        cclass = CClass.values()[(int) (Math.random() * CClass.values().length)];
        template = cclass.default_weapon();
        if (cclass.promotion() != null || cclass == CClass.SOLDIER
            || cclass == CClass.BRIGAND)
          break;
      }

      int[] a = { 8, 2, 2, 2, 0, 1, 1 };
      int points = 18;
      points -= cclass.movement();
      switch (cclass) {
      case MERCENARY:
        a[3] += 3;
        points -= 3;
        break;
      case MYRMIDON:
        a[3] += 4;
        points -= 4;
        break;
      case KNIGHT:
        a[5] += 5;
        points -= 5;
        break;
      case ARCHER:
        a[2] += 3;
        points -= 3;
        break;
      case FIGHTER:
        a[1] += 3;
        a[2] += 3;
        points -= 6;
        break;
      case BRIGAND:
        a[0] += 2;
        a[1] += 3;
        points -= 5;
        break;
      case WYVERN_RIDER:
        a[5] += 4;
        points -= 4;
        break;
      case PEGASUS_KNIGHT:
        a[3] += 4;
        points -= 4;
        break;
      case THIEF:
        a[2] += 2;
        a[3] += 5;
        points -= 7;
        break;
      case SOLDIER:
        a[5] += 4;
        points -= 9;
        break;
      }
      if (template == null || template.is_magical()) {
        a[6] += 4;
        points -= 4;
      }
      if (points < 0)
        throw new Error("Assertion failed!");
      for (int j = 0; j < points; ++j)
        ++a[(int) (Math.random() * a.length)];
      int constitution;
      if (template != null)
        constitution = template.weight() + (a[0] - 8) / 2 + a[5] / 4;
      else
        constitution = 3 + (a[0] - 8) / 2 + a[5] / 4;
      Attributes attr = new Attributes(a[0] * 2
          + (int) (Math.random() * 2), a[1], a[2], a[3], a[4], a[5],
          a[6], constitution);
      opponents[i] = new Combatant("Opponent", cclass, attr, template);

      StaffTemplate staff_template = cclass.default_staff();
      if (staff_template != null)
        opponents[i].acquire(new Staff(staff_template));
    }
    /*
     * Combatant opponent1 = new Combatant("Opponent",CClass.SOLDIER ,new
     * Attributes(19,4,5,4,0,5,1,8)); Combatant opponent2 = new
     * Combatant("Opponent",CClass.SOLDIER ,new
     * Attributes(20,5,5,3,0,5,1,9)); Combatant opponent3 = new
     * Combatant("Opponent",CClass.SOLDIER ,new
     * Attributes(18,4,6,5,0,5,2,7)); Combatant opponent4 = new
     * Combatant("Opponent",CClass.SOLDIER ,new
     * Attributes(20,3,4,3,0,6,3,9)); Combatant opponent5 = new
     * Combatant("Opponent",CClass.SOLDIER ,new
     * Attributes(19,5,6,4,0,4,0,8));
     */

    battle_ = new Battle(terrain);
    int w = battle_.width(), h = battle_.height();
    battle_.addBlue(1, 2, tommy);
    battle_.addBlue(0, 2, lindsey);
    battle_.addBlue(2, 0, daniel);
    battle_.addBlue(3, 0, steven);
    battle_.addBlue(0, 3, ben);
    battle_.addBlue(0, 0, healer);
    // battle_.addRed(w-2,h-2,opponents[0]);
    battle_.addRed(3, 3, opponents[0]);
    battle_.addRed(w - 1, h - 3, opponents[1]);
    battle_.addRed(w - 3, h - 1, opponents[2]);
    battle_.addRed(w - 4, h - 1, opponents[3]);
    battle_.addRed(w - 1, h - 4, opponents[4]);
    // debug
    battle_.mark_gray(tommy);
    battle_.mark_gray(lindsey);
    // battle_.mark_gray(daniel);
    battle_.mark_gray(steven);
    battle_.mark_gray(ben);
  }

  public void run() {
    try {
      do_run();
    } catch (QuitException e) {
    }
    Main.debug.println("Game thread finished.");
  }

  private void do_run() {
    // DEBUG
    {

      Battle testBattle = battle_.clone();
      Combatant enemyTest = testBattle.at(3, 3);
      // testBattle.move(enemyTest,testBattle.loc(1,0));
      //Action atk = Action.attack(testBattle.at(2, 0), testBattle.loc(2, 0), enemyTest);
      //Collection<Action.Outcome> branches = atk.branch_out(testBattle);
      Map<MapLocation,Double> influence = testBattle.influence_map_only_enemies(testBattle.at(3,3), Team.BLUE);
      for (MapLocation m : influence.keySet()) {
        System.out.println("Influence at " + m + ": " + influence.get(m));
      }

      /*
      for (Action.Outcome a : branches) {
        a.testPrint();
        if (a.battle.at(2, 0) != null)
          System.out.println(a.battle.at(2, 0).hp());
        else
          System.out.println("2,0 died!");
        if (a.battle.at(1, 0) != null)
          System.out.println(a.battle.at(1, 0).hp());
        else
          System.out.println("1,0 died!");
      }
      System.out.println("VALUE: " + atk.calc_value(testBattle));
      */
    }

    // this local class is used as a pointer to a Map<MapLocation,Path>
    class MoveRange {
      public Map<MapLocation, Path> content;
    }
    // This movement range belongs to the most recently clicked
    // non-selectable unit.
    // It is displayed during the unit-selection phase (so that the player
    // can check
    // other teams' units' movements ranges).
    final MoveRange soft_selection_move_range = new MoveRange();

    ui_manager_.push(new UI.Decorator(ui_manager_) {
      public void paint(TGraphics tg) {
        Graphics g = tg.get();
        battle_.draw(tg, 0, 0);
        if (soft_selection_move_range.content != null) {
          // debug todo: stick this for loop into a function (it's
          // repeated elsewhere)
          for (MapLocation loc : soft_selection_move_range.content
              .keySet()) {
            g.setColor(new Color(0, 128, 255, 128));
            g.fillRect(loc.x() * Debug.GRID, loc.y() * Debug.GRID
                + 1, Debug.GRID - 1, Debug.GRID - 1);
          }
        }
        if (battle_.valid_coordinate(mouse_x() / Debug.GRID, mouse_y()
            / Debug.GRID)) {
          Combatant highlighted = battle_.at(mouse_x() / Debug.GRID,
              mouse_y() / Debug.GRID);
          if (highlighted != null)
            highlighted.draw_info(g, battle_.width() * Debug.GRID
                + 20, 20);
        }
      }
    });
    try {
      for (;;) {
        if (battle_.phase() != Team.BLUE) { // not player's turn
          ai_.decide(battle_.clone()).perform(battle_);
          continue;
        }

        // debug
        // {
        // //unit test for Battle.successors()
        // for(Action a: battle_.successors())
        // Main.log.println(a);
        // Main.log.println("Above is the list of available moves.");
        // }

        Main.debug.println("Who would you like to move?");
        final MapLocation source_location = ui_manager_
            .await_location(battle_);
        if (source_location == null) { // user canceled
          soft_selection_move_range.content = null;
          continue;
        }

        // Main.debug.println(source_location);
        // Main.debug.println(battle_);

        final Combatant selected = battle_.at(source_location);
        // if(selected != null)
        // Main.debug.println("Selected's team = "+battle_.team(selected));

        // check other teams' units' move range
        if (selected != null
            && battle_.team(selected) != battle_.phase())
          soft_selection_move_range.content = battle_
          .move_range(selected);
        else
          soft_selection_move_range.content = null;

        if (selected != null
            && battle_.team(selected) == battle_.phase()
            && battle_.hasnt_moved(selected)) {
          final Map<MapLocation, Path> move_range = battle_
              .move_range(selected);

          Main.log.println("Selected " + selected.name());
          for (;;) {
            Main.debug.println("Where would you like to move?");
            final MapLocation destination;
            ui_manager_.push(new UI.Decorator(ui_manager_) {
              public void paint(TGraphics tg) {
                super.paint(tg);
                Graphics g = tg.get();
                // debug todo: stick this for loop into a
                // function (it's repeated elsewhere)
                for (MapLocation loc : move_range.keySet()) {
                  g.setColor(new Color(0, 128, 255, 128));
                  g.fillRect(loc.x() * Debug.GRID, loc.y()
                      * Debug.GRID + 1, Debug.GRID - 1,
                      Debug.GRID - 1);
                }
                Util.draw_cursor(g, source_location.x()
                    * Debug.GRID, source_location.y()
                    * Debug.GRID);
              }
            });
            try {
              destination = ui_manager_.await_location(battle_);
            } finally {
              ui_manager_.pop();
            }
            if (destination == null) // user canceled
              break; // go back to unit selection
            if (destination != source_location
                && (battle_.at(destination) != null || !move_range
                .containsKey(destination)))
              continue; // ignore this illegal click (redo
            // movement selection)
            List<Command> menu = new ArrayList<Command>();
            for (Command c : Command.values())
              if (c.usable(battle_, selected, destination))
                menu.add(c);
            String[] choices = new String[menu.size()];
            for (int i = 0; i < choices.length; ++i)
              choices[i] = menu.get(i).getName();
            boolean fully_confirmed = false;
            for (;;) {
              Main.debug.println("What would you like to do?");
              Integer choice;
              ui_manager_.push(new UI.Decorator(ui_manager_) {
                public void paint(TGraphics tg) {
                  Graphics g = tg.get();
                  battle_.draw(tg, 0, 0, selected);
                  selected.draw(tg, destination.x(),
                      destination.y(),
                      battle_.team(selected).palette());
                  Util.draw_cursor(g, destination.x()
                      * Debug.GRID, destination.y()
                      * Debug.GRID);
                }
              });
              try {
                choice = ui_manager_.await_choice(
                    "What would you like to do?", choices,
                    battle_.width() * Debug.GRID + 5, 5);
                if (choice == null) // user canceled
                  break; // go back to movement selection
                if (menu.get(choice).use(battle_, ui_manager_,
                    selected, destination)) {
                  fully_confirmed = true;
                  break; // (ultimately) go back to unit
                  // selection
                }
              } finally {
                ui_manager_.pop();
              }
            }
            if (fully_confirmed)
              break; // go back to unit selection
          }
        }
      }
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new Game.QuitException("Quitting the game.", e);
    } finally {
      ui_manager_.pop();
    }
  }

  public UI.Manager ui() {
    return ui_manager_;
  }

  // nested class
  public static class QuitException extends RuntimeException {
    public QuitException(String s, Throwable t) {
      super(s, t);
    }
  }

  // fields
  private UI.Manager ui_manager_;
  private AI ai_;
  private final Battle battle_;
}

// //////////////////////////////////////////////////////////////

abstract class UI {
  abstract public void paint(TGraphics g);

  // all methods return:
  // UI.IGNORE, if the enclosing context should not be interested in this
  // event
  // a value that will be interesting to the enclosing context, otherwise
  abstract public Object mouse_pressed(int button, int x, int y);

  abstract public Object mouse_moved(int x, int y);

  abstract public Object mouse_dragged(int x, int y);

  // nested classes
  public static final Object IGNORE = new Object();

  public static final class Manager {
    // ctor
    Manager(Applet main) {
      main_ = main;
    }

    // accessors
    public Decorated get_delegate() {
      return delegate_;
    }

    public int mouse_x() {
      return mouse_x_;
    }

    public int mouse_y() {
      return mouse_y_;
    }

    // public methods
    public void push(Decorated delegate) {
      if (delegate.get_delegate() != delegate_)
        throw new Error("Assertion failed!");
      delegate_ = delegate;
    }

    public void pop() {
      delegate_ = delegate_.get_delegate();
      if (delegate_ == null)
        throw new Error("Assertion failed! UI stack underflow.");
    }

    // public utility functions
    public void push_event(AWTEvent ev) {
      try {
        queue_.put(ev);
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new Error("Assertion failed!", e);
      }
    }

    public Object await_value(UI.Decorated ui) throws InterruptedException {
      push(ui);
      try {
        for (;;) {
          Object result = dispatch(queue_.take());
          if (result != IGNORE)
            return result;
        }
      } finally {
        pop();
      }
    }

    public MapLocation await_location(final Battle context)
        throws InterruptedException {
      return (MapLocation) await_value(new UI.Decorator(this) {
        public Object mouse_pressed(int button, int x, int y) {
          if (button == 3) // right button
            return null; // indicates that user canceled
          else if (button == 1) { // left button
            // did we click on the battlefield, or outside it?
            if (x < Debug.GRID * context.width()
                && y < Debug.GRID * context.height())
              return context.loc(x / Debug.GRID, y / Debug.GRID);
            else {
              Object ignored = super.mouse_pressed(button, x, y);
              if (ignored != IGNORE)
                throw new Error("Assertion failed!");
              return IGNORE;
            }
          } else
            return IGNORE;
        }

        public void paint(TGraphics g) {
          super.paint(g);
          if (context.valid_coordinate(mouse_x() / Debug.GRID,
              mouse_y() / Debug.GRID))
            Util.draw_cursor(g.get(), mouse_x() / Debug.GRID
                * Debug.GRID, mouse_y() / Debug.GRID
                * Debug.GRID);
        }
      });
    }

    public Integer await_choice(final String prompt, final String[] menu,
        final int menu_x, final int menu_y) throws InterruptedException {
      final int OPTION_HEIGHT = 20;
      final int MENU_WIDTH = 200;
      return (Integer) await_value(new UI.Decorator(this) {
        public Object mouse_pressed(int button, int x, int y) {
          if (button == 3) // right button
            return null; // indicates that user canceled
          else if (button == 1) { // left button
            // did we click within the menu, or outside it?
            if (x >= menu_x
                && x < menu_x + MENU_WIDTH
                && y >= menu_y + OPTION_HEIGHT
                && y < menu_y + OPTION_HEIGHT
                * (menu.length + 1))
              return (y - menu_y) / OPTION_HEIGHT - 1;
            else
              return IGNORE;
          } else
            return IGNORE;
        }

        public void paint(TGraphics tg) {
          final int PAD = 3;
          super.paint(tg);
          Graphics g = tg.get();
          g.setColor(new Color(0, 0, 0, 192));
          g.fillRect(menu_x, menu_y, MENU_WIDTH, (menu.length + 1)
              * OPTION_HEIGHT);
          int x = mouse_x(), y = mouse_y();
          if (x >= menu_x && x < menu_x + MENU_WIDTH
              && y >= menu_y + OPTION_HEIGHT
              && y < menu_y + OPTION_HEIGHT * (menu.length + 1)) {
            g.setColor(Color.BLUE);
            int i = (y - menu_y) / OPTION_HEIGHT;
            g.fillRect(menu_x + PAD, menu_y + i * OPTION_HEIGHT
                + PAD, MENU_WIDTH - 2 * PAD, OPTION_HEIGHT - 2
                * PAD);
          }
          g.setColor(Color.WHITE);
          g.drawString(prompt, menu_x + PAD, menu_y + 3
              * OPTION_HEIGHT / 4);
          for (int i = 0; i < menu.length; ++i)
            g.drawString(menu[i], menu_x + PAD, menu_y
                + (4 * (i + 1) + 3) * OPTION_HEIGHT / 4);
        }
      });
    }

    public void paint(TGraphics g) {
      delegate_.paint(g);
    }

    // private methods
    private Object dispatch(AWTEvent ev) {
      switch (ev.getID()) {
      case MouseEvent.MOUSE_MOVED: {
        MouseEvent me = (MouseEvent) ev;
        int x = me.getX(), y = me.getY();
        update_mouse(x, y);
        return delegate_.mouse_moved(x, y);
      } // implicit break
      case MouseEvent.MOUSE_DRAGGED: {
        MouseEvent me = (MouseEvent) ev;
        int x = me.getX(), y = me.getY();
        update_mouse(x, y);
        return delegate_.mouse_dragged(x, y);
      } // implicit break
      case MouseEvent.MOUSE_PRESSED: {
        MouseEvent me = (MouseEvent) ev;
        int button = me.getButton(), x = me.getX(), y = me.getY();
        return delegate_.mouse_pressed(button, x, y);
      } // implicit break
      default: {
        throw new Error("Assertion failed!");
      } // implicit break
      }
    }

    /*
     * private Object set_result(Object result) { synchronized(lock_) {
     * result_ = result; if(result != IGNORE) lock_.notify(); } return null;
     * }
     */
    private void update_mouse(int x, int y) {
      mouse_x_ = x;
      mouse_y_ = y;
    }

    public void draw_image(Graphics g, String filename, int x, int y,
        int w, int h) {
      draw_image(g, filename, x, y, w, h, ColorMapping.NULL);
    }

    public void draw_image(Graphics g, String filename, int x, int y,
        int w, int h, ColorMapping cm) {
      String key = filename + " separator " + cm;
      if (!image_cache_.containsKey(key)) {
        Image image = main_.getImage(main_.getDocumentBase(),
            "data/image/" + filename);
        MediaTracker mt = new MediaTracker(main_);
        mt.addImage(image, 0);
        try {
          mt.waitForAll();
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
          throw new Error("Assertion failed!", e);
        }
        image = cm.apply_to(image);
        image_cache_.put(key, image);
        Main.debug.println("Loaded image: " + key);
      }
      g.drawImage(image_cache_.get(key), x, y, w, h, main_);
    }

    // fields
    private Object lock_ = new Object();
    private Object result_;
    private Decorated delegate_ = new Simple();
    private int mouse_x_;
    private int mouse_y_;
    private BlockingQueue<AWTEvent> queue_ = new LinkedBlockingQueue<AWTEvent>();
    private Applet main_;
    private Map<String, Image> image_cache_ = new HashMap<String, Image>();
  }

  public static class TGraphics {
    public TGraphics(Graphics g, Manager manager) {
      graphics_ = g;
      manager_ = manager;
    }

    public void draw_image(String filename, int x, int y, int w, int h) {
      manager_.draw_image(graphics_, filename, x, y, w, h);
    }

    public void draw_image(String filename, int x, int y, int w, int h,
        ColorMapping cm) {
      manager_.draw_image(graphics_, filename, x, y, w, h, cm);
    }

    public Graphics get() {
      return graphics_;
    }

    private Graphics graphics_; // delegate
    private Manager manager_; // another delegate
  }

  public static abstract class Decorator extends Decorated {
    // ctor
    public Decorator(Manager manager) {
      super(manager.get_delegate());
      manager_ = manager;
    }

    // protected methods
    protected final int mouse_x() {
      return manager_.mouse_x();
    }

    protected final int mouse_y() {
      return manager_.mouse_y();
    }

    // UI overrides
    public void paint(TGraphics g) {
      get_delegate().paint(g);
    }

    public Object mouse_pressed(int button, int x, int y) {
      return get_delegate().mouse_pressed(button, x, y);
    }

    public Object mouse_moved(int x, int y) {
      return get_delegate().mouse_moved(x, y);
    }

    public Object mouse_dragged(int x, int y) {
      return get_delegate().mouse_dragged(x, y);
    }

    // fields
    private Manager manager_;
  }

  private static abstract class Decorated extends UI {
    public Decorated(Decorated delegate) {
      delegate_ = delegate;
    }

    public final Decorated get_delegate() {
      return delegate_;
    }

    private final Decorated delegate_;
  }

  private static class Simple extends Decorated {
    public Simple() {
      super(null);
    }

    public Object mouse_pressed(int button, int x, int y) {
      return IGNORE;
    }

    public Object mouse_moved(int x, int y) {
      return IGNORE;
    }

    public Object mouse_dragged(int x, int y) {
      return IGNORE;
    }

    public void paint(TGraphics g) {
    }
  }
}

// //////////////////////////////////////////////////////////////

class ColorMapping extends HashMap<Integer, Integer> {
  public static final ColorMapping NULL;
  public static final ColorMapping BLUE;
  public static final ColorMapping RED;
  public static final ColorMapping GREEN;
  public static final ColorMapping GRAY;
  public static final ColorMapping MAGENTA;

  public ColorMapping() {
    super();
  }

  /*
   * public Color get(Color key) { Color result = super.get(key); if(result ==
   * null) result = key; return result; }
   */
  public Image apply_to(Image image) {
    BufferedImage buffered_image = new BufferedImage(image.getWidth(null),
        image.getHeight(null), BufferedImage.TYPE_INT_ARGB);
    Graphics g = buffered_image.getGraphics();
    g.drawImage(image, 0, 0, null);
    for (int i = 0; i < image.getHeight(null); ++i) {
      for (int j = 0; j < image.getWidth(null); ++j) {
        int old_color = buffered_image.getRGB(j, i);
        Integer new_color = get(old_color);
        if (new_color != null) {
          buffered_image.setRGB(j, i, new_color);
        }
      }
    }
    return buffered_image;
  }

  // implementation details
  private static final int[][] PALETTE = {
    { 0xffd8e8f0, 0xffb8b8b8, 0xffd8e8f0, 0xffe0e0e0, 0xff00ff00 } // White
    , { 0xff90b8e8, 0xff787878, 0xff90b8e8, 0xffc0a8b8, 0xff00ff00 } // Pale
    , { 0xff18f0f8, 0xffb8b8b8, 0xff18f0f8, 0xfff85048, 0xff00ff00 } // Bright
    , { 0xff28a0f8, 0xff989898, 0xff28a0f8, 0xffe01010, 0xff00ff00 } // Light
    , { 0xff3850e0, 0xff585858, 0xff3850e0, 0xffa83028, 0xff00ff00 } // Medium
    , { 0xff4850e0, 0xff585858, 0xff3850e0, 0xffa83028, 0xff00ff00 } // Medium
    , { 0xff383890, 0xff484848, 0xff383890, 0xff602820, 0xff00ff00 } // Dark
    , { 0xff584878, 0xff404040, 0xff584878, 0xff684860, 0xff00ff00 } // Muted
    , { 0xffff00ff, 0xff7f7f7f, 0xff0000ff, 0xffff0000, 0xff00ff00 } // default
    // colors
  };
  static {
    NULL = new ColorMapping();
    BLUE = NULL;
    RED = new ColorMapping(3);
    GREEN = new ColorMapping(4);
    GRAY = new ColorMapping(1);
    MAGENTA = new ColorMapping(0);
  }

  private ColorMapping(int index) {
    for (int[] a : PALETTE)
      put(a[2], a[index]);
  }
}

// //////////////////////////////////////////////////////////////

class Debug {
  public static final int GRID = 32;

  public static int menu_x(Battle context) {
    return context.width() * GRID + 5;
  }

  public static int menu_y(Battle context) {
    return 5;
  }

  private Debug() {
    throw new UnsupportedOperationException();
  }
}

// //////////////////////////////////////////////////////////////

class TwoWayMap<A, B> {
  public TwoWayMap() {
  }

  public TwoWayMap(TwoWayMap<A, B> to_copy) {
    a_.putAll(to_copy.a_);
    b_.putAll(to_copy.b_);
  }

  public String toString() {
    return b_.toString();
  }

  public Set<A> keySet() {
    return b_.keySet();
  }

  public Set<B> valueSet() {
    return a_.keySet();
  }

  public A getKey(B b) {
    return a_.get(b);
  }

  public B getValue(A a) {
    return b_.get(a);
  }

  public void put(A a, B b) {
    a_.remove(b_.get(a));
    b_.remove(a_.get(b));
    a_.put(b, a);
    b_.put(a, b);
  }

  public void removeByKey(A a) {
    a_.remove(b_.get(a));
    b_.remove(a);
  }

  public void removeByValue(B b) {
    b_.remove(a_.get(b));
    a_.remove(b);
  }

  private Map<B, A> a_ = new HashMap<B, A>();
  private Map<A, B> b_ = new HashMap<A, B>();
}

// //////////////////////////////////////////////////////////////

enum Team {
  // enumerated list
  BLUE(Color.BLUE, "Player", ColorMapping.BLUE), RED(Color.RED, "Enemy",
      ColorMapping.RED), GREEN(Color.GREEN, "Other", ColorMapping.GREEN);

  // public methods
  public Color color() {
    return color_;
  }

  public String phase_string() {
    return phase_string_;
  }

  public boolean is_enemy_of(Team t) {
    return (this == RED) != (t == RED);
  } // debug stub

  public Team next() {
    return values()[(ordinal() + 1) % values().length];
  }

  public ColorMapping palette() {
    return palette_;
  }

  // private ctor
  private Team(Color color, String phase_string, ColorMapping palette) {
    color_ = color;
    phase_string_ = phase_string;
    palette_ = palette;
  }

  // fields
  private Color color_;
  private String phase_string_;
  private ColorMapping palette_;
}

// //////////////////////////////////////////////////////////////

class Battle implements Iterable<Combatant> {
  public Battle(Terrain[][] terrain) {
    int width = terrain.length;
    int height = terrain[0].length;
    locations_ = new MapLocation[height][width];
    for (int i = 0; i < height; ++i)
      for (int j = 0; j < width; ++j)
        locations_[i][j] = new MapLocation(j, i, terrain[i][j]);

    for (Team t : Team.values())
      roster_.put(t, new HashSet<Combatant>());
  }

  // accessors
  public int width() {
    return locations_[0].length;
  }

  public int height() {
    return locations_.length;
  }

  public Combatant at(int x, int y) {
    return unit_map_.getValue(locations_[y][x]);
  }

  public Combatant at(MapLocation loc) {
    return unit_map_.getValue(loc);
  }

  public Combatant at(Combatant c) {
    return unit_map_.getValue(rescue_slot_.getValue(c));
  }

  public MapLocation loc(Combatant c) {
    return unit_map_.getKey(c);
  }

  public MapLocation loc(int x, int y) {
    return locations_[y][x];
  }

  public Team team(Combatant c) {
    return team_.get(c);
  }

  public Team phase() {
    return phase_;
  }

  public Set<Combatant> roster(Team team) {
    return roster_.get(team);
  }

  public Set<Combatant> movable_set() {
    return hasnt_moved_;
  }

  public boolean hasnt_moved(Combatant c) {
    return hasnt_moved_.contains(c);
  }

  // mutators
  public void addBlue(int x, int y, Combatant c) {
    addUnit(locations_[y][x], c, Team.BLUE);
  }

  public void addRed(int x, int y, Combatant c) {
    addUnit(locations_[y][x], c, Team.RED);
  }

  public void remove(Combatant c) {
    if (at(rescue_slot_.getValue(c)) != null)
      throw new Error("Assertion failed! Rescue slot not empty.");
    unit_map_.removeByValue(c);
    rescue_slot_.removeByKey(c);
    roster_.get(team_.get(c)).remove(c);
    team_.remove(c);
    hasnt_moved_.remove(c);
  }

  public void mark_gray(Combatant c) {
    hasnt_moved_.remove(c);
    while (hasnt_moved_.isEmpty()) {
      phase_ = phase_.next();
      hasnt_moved_.addAll(roster_.get(phase_));
      // Main.log.println("Phase = "+phase_);
    }
  }

  public void attack(Combatant source, Combatant target) {
    int distance = distance(loc(source), loc(target));
    int my_speed = source.attack_speed();
    int other_speed = target.attack_speed();
    Weapon src_wep = source.weapon();
    Weapon tgt_wep = target.weapon();

    if (!src_wep.within_range(distance))
      throw new Error("Assertion failed!"); // either too far away or too
    // close
    if (!src_wep.can_be_used_by(source))
      throw new Error("Assertion failed!"); // lacking weapon skill

    source.attack(this, target);

    if (tgt_wep.within_range(distance)) {
      target.attack(this, source);
      if (my_speed >= other_speed + 4) {
        source.attack(this, target);
      } else if (other_speed >= my_speed + 4) {
        target.attack(this, source);
      }
    } else {
      if (my_speed >= other_speed + 4)
        source.attack(this, target);
    }
  }

  public void move(Combatant c, MapLocation destination) {
    unit_map_.put(destination, c);
  }

  public void make_ai_move() { // archaic method
    Combatant c = hasnt_moved_.iterator().next();
    Main.log.println("AI is moving: " + c.name());
    Map<MapLocation, Path> move_range = move_range(c);
    double best_priority = 0.0;
    MapLocation best_destination = null;
    Combatant best_target = null;
    for (MapLocation destination : move_range.keySet()) {
      if (at(destination) != null && !at(destination).equals(c))
        continue;
      Combatant target = null;
      double target_priority = 0.0;
      double destination_priority = 0.0;
      for (Combatant enemy : this)
        if (team(c).is_enemy_of(team(enemy))) {
          destination_priority -= distance(destination, loc(enemy));
          if (c.weapon().within_range(destination, loc(enemy))) {
            Battle temp_battle = new Battle(this);
            temp_battle.move(c, destination);
            Combatant.CombatInfo my_info = new Combatant.CombatInfo(
                temp_battle, c, enemy);
            Combatant.CombatInfo enemy_info = new Combatant.CombatInfo(
                temp_battle, enemy, c);
            double my_effectiveness = 100.0
                * my_info.expected_damage() / enemy.hp();
            double enemy_effectiveness = 30.0
                * enemy_info.expected_damage() / c.hp();
            double curr_target_priority = my_effectiveness
                - enemy_effectiveness;
            Main.debug.println("  Considering attacking "
                + enemy.name() + ": " + my_effectiveness
                + " - " + enemy_effectiveness);
            Main.debug
            .println("    " + my_info + ". " + enemy_info);
            if (curr_target_priority > target_priority) {
              target_priority = curr_target_priority;
              target = enemy;
            }
          }
        }
      double priority = destination_priority + target_priority;
      if (best_destination == null || priority > best_priority) {
        Main.debug.println("  Best destination = " + destination
            + " with priority = " + priority);
        best_destination = destination;
        best_priority = priority;
        best_target = target;
      } else {
        Main.debug.println("  Igoring potential destination = "
            + destination + " with priority = " + priority);
      }
    }
    Main.debug.println("  Moving to " + best_destination + " ...");
    move(c, best_destination);
    if (best_target != null)
      attack(c, best_target);
    mark_gray(c);
  }

  // constant methods
  public Collection<Action> successors() {
    Collection<Action> result = new LinkedList<Action>();
    for (Combatant c : movable_set()) {
      Map<MapLocation, Path> move_range = move_range(c);
      for (MapLocation dest : move_range.keySet()) {
        if (at(dest) != null && !at(dest).equals(c))
          continue;
        result.add(Action.wait(c, dest));
        Battle temp_context = clone();
        temp_context.move(c, dest);
        for (Combatant target : this) {
          if (c.can_attack(temp_context, target))
            result.add(Action.attack(c, dest, target));
          if (c.can_use_staff(temp_context, target))
            result.add(Action.staff(c, dest, target));
        }
      }
    }
    return result;
  }
  
  public Collection<Action> successor_attacks() {
    Collection<Action> result = new LinkedList<Action>();
    for (Combatant c : movable_set()) {
      Map<MapLocation, Path> move_range = move_range(c);
      for (MapLocation dest : move_range.keySet()) {
        if (at(dest) != null && !at(dest).equals(c))
          continue;
        Battle temp_context = clone();
        temp_context.move(c, dest);
        for (Combatant target : this) {
          if (c.can_attack(temp_context, target))
            result.add(Action.attack(c, dest, target));
        }
      }
    }
    return result;
  }

  public Iterator<Combatant> iterator() { // iterates ALL Combatants in the
    // Battle
    return new Iterator<Combatant>() {
      public boolean hasNext() {
        return set_itor_ != null;
      }

      public Combatant next() {
        Combatant result = set_itor_.next();
        while (!set_itor_.hasNext()) {
          if (roster_itor_.hasNext())
            set_itor_ = roster_itor_.next().iterator();
          else {
            set_itor_ = null;
            break;
          }
        }
        return result;
      }

      public void remove() {
        throw new UnsupportedOperationException();
      }

      private Iterator<Set<Combatant>> roster_itor_;
      private Iterator<Combatant> set_itor_; // != null iff this.hasNext()
      /* initializer */{
        roster_itor_ = roster_.values().iterator();
        if (roster_itor_.hasNext())
          set_itor_ = roster_itor_.next().iterator();
        else
          set_itor_ = null;
        while (!set_itor_.hasNext()) {
          if (roster_itor_.hasNext())
            set_itor_ = roster_itor_.next().iterator();
          else {
            set_itor_ = null;
            break;
          }
        }
      }
    };
  }

  public boolean valid_coordinate(int x, int y) {
    return y >= 0 && y < locations_.length && x >= 0
        && x < locations_[y].length;
  }

  public int distance(MapLocation a, MapLocation b) {
    return a.distance_to(b);
  }

  public String toString() {
    String result = "";
    for (MapLocation[] a : locations_) {
      for (MapLocation loc : a) {
        if (unit_map_.getValue(loc) == null)
          result += "-";
        else
          result += unit_map_.getValue(loc).name().charAt(0);
        result += " ";
      }
      result += "\n";
    }
    result += unit_map_;
    return result;
  }

  public void draw(UI.TGraphics tg, int x, int y) {
    draw(tg, x, y, null);
  }

  public void draw(UI.TGraphics tg, int x, int y, Combatant except) {
    Graphics g = tg.get();
    for (int i = 0; i < locations_.length; ++i) {
      for (int j = 0; j < locations_[i].length; ++j) {
        MapLocation loc = locations_[i][j];

        // draw terrain at that location
        loc.terrain().draw(g, x + j, y + i);

        // draw combatant at that location
        Combatant c = unit_map_.getValue(loc);
        if (c != null && (except == null || !c.equals(except))) {
          if (team(c) != phase_ || hasnt_moved_.contains(c))
            c.draw(tg, x + j, y + i, team_.get(c).palette());
          else {
            c.draw(tg, x + j, y + i, ColorMapping.GRAY);
          }
        }
      }
    }

    // draw all hp bars
    for (int i = 0; i < locations_.length; ++i) {
      for (int j = 0; j < locations_[i].length; ++j) {
        MapLocation loc = locations_[i][j];
        Combatant c = unit_map_.getValue(loc);
        if (c != null && (except == null || !c.equals(except)))
          c.draw_hp_bar(tg, x + j, y + i, team_.get(c).color());
      }
    }

    // draw info
    g.setColor(phase_.color());
    g.drawString(phase_.phase_string() + " Phase", 20, 20 + height()
        * Debug.GRID);
  }

  public MapLocation neighbor(MapLocation loc, Dir d) {
    if (loc.is_null())
      throw new Error("Assertion failed!");
    int x = loc.x() + d.getH();
    int y = loc.y() + d.getV();
    if (!valid_coordinate(x, y))
      return null;
    return locations_[y][x];
  }

  public boolean location_isnt_threatened(MapLocation loc, Team enemy_team) {
    if (loc.is_null())
      throw new Error("Assertion failed!");
    for (Combatant enemy : roster(enemy_team)) {
      for (MapLocation possible_loc : move_range(enemy).keySet()) {
        if (enemy.weapon().within_range(possible_loc, loc))
          return false;
      }
    }
    return true;
  }

  public Map<MapLocation,Double> influence_map_only_enemies(Combatant person, Team enemy_team) {
    Map<MapLocation,Double> result = new HashMap<MapLocation,Double>();
    for (MapLocation target_loc : move_range(person).keySet()) {
      if (!result.containsKey(target_loc)) result.put(target_loc, 0.0);
      result.put(target_loc, result.get(target_loc) + location_threat(target_loc,person,enemy_team));
    }
    return result;
  }

  public double location_threat(MapLocation target, Combatant player, Team enemy_team) {
    double result = 0.0;
    Battle clone = this.clone();
    clone.move(player, target);
    for (Combatant enemy : clone.roster(enemy_team)) {
      Combatant r_enemy = enemy;
      for (MapLocation candidate : clone.move_range(r_enemy).keySet()) {
        Battle subclone = clone.clone();
        Combatant real_player = subclone.at(subclone.loc(player));
        Combatant real_enemy = subclone.at(subclone.loc(r_enemy));
        subclone.move(real_enemy, candidate);
        if (real_enemy.can_attack(subclone, real_player)) {
          Combatant.CombatInfo ci = new Combatant.CombatInfo(
              subclone, enemy, real_player);
          result += ci.expected_damage();
          // TODO: Break the for loop to go to next enemy once a suitable attack point has been found.
        }
      }
    }
    return result;
  }

  public Collection<MapLocation> attack_range(Combatant c) {
    class Pair { public int x,y; Pair(int xt, int yt) { x=xt; y=yt; } }
    Collection<Pair> temp = new HashSet<Pair>();
    Collection<MapLocation> result = new HashSet<MapLocation>();
    for (MapLocation loc : move_range(c).keySet()) {
      int x = loc.x();
      int y = loc.y();
      int min = c.weapon().min_range();
      int max = c.weapon().max_range();
      for (int i = min; i <= max; i++) {
        temp.add(new Pair(x-i,y));
        temp.add(new Pair(x+i,y));
        temp.add(new Pair(x,y-i));
        temp.add(new Pair(x,y+i));
        for (int j = 1; j < i; j++) {
          temp.add(new Pair(x-i+j, y-j));
          temp.add(new Pair(x-i+j, y+j));
          temp.add(new Pair(x+i-j, y-j));
          temp.add(new Pair(x+i-j, y+j));
        }
      }
    }
    for (Pair p : temp)
      if (valid_coordinate(p.x, p.y)) result.add(loc(p.x,p.y));
    return result;
  }
  
  public Map<MapLocation, Path> move_range(Combatant c) {
    Map<MapLocation, Path> result = new HashMap<MapLocation, Path>();
    Set<MapLocation> frontier = new HashSet<MapLocation>();

    result.put(loc(c), new Path(new Dir[0], 0));
    frontier.add(loc(c));

    while (frontier.size() > 0) {
      Set<MapLocation> new_frontier = new HashSet<MapLocation>();
      for (MapLocation loc : frontier) {
//        Main.debug.println("Frontier Cell: " + loc.x() + " " + loc.y());
        for (Dir d : Dir.values()) {
          MapLocation next_loc = neighbor(loc, d);
          if (next_loc == null) // map edge
            continue;
          if (at(next_loc) != null
              && team(at(next_loc)).is_enemy_of(team(c))) // blocked
            // by
            // enemy
            // unit
            continue;
          int move_cost = c.cclass().move_cost(next_loc.terrain());
          if (move_cost == -1) // impassable terrain
            continue;
          move_cost += result.get(loc).cost();
          Main.debug
          .println("  Potential Cell: " + next_loc.x() + " "
              + next_loc.y() + "; Total cost: "
              + move_cost);
          if (move_cost <= c.movement()) {
            if (result.get(next_loc) == null
                || move_cost < result.get(next_loc).cost()) {
              Dir[] array = result.get(loc).array();
              Dir[] next_array = new Dir[array.length + 1];
              System.arraycopy(array, 0, next_array, 0,
                  array.length);
              next_array[next_array.length - 1] = d;
              result.put(next_loc,
                  new Path(next_array, move_cost));
              new_frontier.add(next_loc);
              Main.debug.println("    Confirmed");
            } else {
              Main.debug
              .println("    Discarded - not better than "
                  + result.get(next_loc).cost());
            }
          } else {
            Main.debug.println("    Out of range");
          }
        }
      }
      frontier = new_frontier;
      Main.debug.println("Frontier Count: " + frontier.size());
    }
    Main.debug.println("move_range.size() = " + result.size());
    return result;
  }

  public Battle clone() {
    return new Battle(this);
  }

  // implementation details
  private Battle(Battle to_copy) {
    locations_ = to_copy.locations_;
    Map<Combatant, Combatant> new_version = new HashMap<Combatant, Combatant>();
    for (Combatant c : to_copy)
      new_version.put(c, c.clone());
    for (Combatant c : to_copy.unit_map_.valueSet())
      unit_map_.put(to_copy.unit_map_.getKey(c), new_version.get(c));
    for (Combatant c : to_copy.rescue_slot_.keySet())
      rescue_slot_.put(new_version.get(c),
          to_copy.rescue_slot_.getValue(c));
    for (Combatant c : to_copy.team_.keySet())
      team_.put(new_version.get(c), to_copy.team_.get(c));
    for (Combatant c : to_copy.hasnt_moved_)
      hasnt_moved_.add(new_version.get(c));
    for (Team t : to_copy.roster_.keySet()) {
      Set<Combatant> team_roster = new HashSet<Combatant>();
      for (Combatant c : to_copy.roster_.get(t))
        team_roster.add(new_version.get(c));
      roster_.put(t, team_roster);
    }
    phase_ = to_copy.phase_;
  }

  private void addUnit(MapLocation dest, Combatant c, Team t) {
    if (unit_map_.getValue(dest) != null || unit_map_.getKey(c) != null)
      throw new Error("Assertion failed!");
    unit_map_.put(dest, c);
    rescue_slot_.put(c, new MapLocation());
    team_.put(c, t);
    roster_.get(t).add(c);
    if (t == phase_)
      hasnt_moved_.add(c);
  }

  private MapLocation[][] locations_;
  private TwoWayMap<MapLocation, Combatant> unit_map_ = new TwoWayMap<MapLocation, Combatant>();
  private TwoWayMap<Combatant, MapLocation> rescue_slot_ = new TwoWayMap<Combatant, MapLocation>();
  private Map<Combatant, Team> team_ = new HashMap<Combatant, Team>();
  private Map<Team, Set<Combatant>> roster_ = new EnumMap<Team, Set<Combatant>>(
      Team.class);
  private Set<Combatant> hasnt_moved_ = new HashSet<Combatant>();
  private Team phase_ = Team.BLUE;
}

// //////////////////////////////////////////////////////////////

enum Command {
  // enumerated list
  ATTACK("Attack") {
    public boolean usable(Battle context, Combatant c, MapLocation dest) {
      for (Team t : Team.values())
        if (t.is_enemy_of(context.team(c)))
          for (Combatant enemy : context.roster(t))
            if (c.weapon().within_range(dest, context.loc(enemy)))
              return true;
      return false;
    }

    public boolean use(final Battle context, UI.Manager ui,
        final Combatant c, final MapLocation dest) {
      final Battle temp_context = context.clone();
      temp_context.move(c, dest);
      ui.push(new UI.Decorator(ui) {
        public void paint(TGraphics tg) {
          super.paint(tg);
          Graphics g = tg.get();
          Combatant highlighted = null;
          g.setColor(new Color(255, 0, 0, 128));
          for (Team t : Team.values())
            if (t.is_enemy_of(context.team(c)))
              for (Combatant enemy : context.roster(t))
                if (c.weapon().within_range(dest,
                    context.loc(enemy))) {
                  g.fillRect(context.loc(enemy).x()
                      * Debug.GRID, context.loc(enemy)
                      .y() * Debug.GRID + 1,
                      Debug.GRID - 1, Debug.GRID - 1);
                  if (context.valid_coordinate(mouse_x()
                      / Debug.GRID, mouse_y()
                      / Debug.GRID)) {
                    Combatant moused_over = context.at(
                        mouse_x() / Debug.GRID,
                        mouse_y() / Debug.GRID);
                    if (moused_over != null
                        && moused_over.equals(enemy))
                      highlighted = enemy;
                  }
                }
          if (highlighted != null) {
            g.setColor(Color.BLACK);
            c.draw_combat_info(g,
                context.width() * Debug.GRID + 20, 20,
                temp_context, highlighted);
          }
        }
      });
      try {
        // weapon selection menu //debug stub

        for (;;) {
          Main.debug.println("Whom would you like to attack?");
          MapLocation target_loc = ui.await_location(context);

          if (target_loc == null)
            return false;
          if (c.weapon().within_range(dest, target_loc)) {
            Combatant target = context.at(target_loc);
            if (target != null
                && context.team(target).is_enemy_of(
                    context.team(c))) {
              Action.attack(c, dest, target).perform(context);
              return true;
            }
          }
        }
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new Game.QuitException("Quitting the game.", e);
      } finally {
        ui.pop();
      }
    }
  },
  STAFF("Staff") {
    public boolean usable(Battle context, Combatant c, MapLocation dest) {
      Battle temp_context = context.clone();
      temp_context.move(c, dest);
      for (Combatant target : temp_context)
        if (!target.equals(c) && c.can_use_staff(temp_context, target))
          return true;
      return false;
    }

    public boolean use(final Battle context, final UI.Manager ui,
        final Combatant c, final MapLocation dest) {
      final Battle temp_context = context.clone();
      temp_context.move(c, dest);
      MapLocation target_loc;

      // staff selection dialog //debug stub

      for (;;) {
        ui.push(new UI.Decorator(ui) {
          public void paint(TGraphics tg) {
            super.paint(tg);
            Graphics g = tg.get();
            g.setColor(new Color(0, 255, 0, 128));
            for (Combatant person : context)
              if (!person.equals(c)
                  && c.staff()
                  .usable(temp_context, c, person))
                g.fillRect(
                    context.loc(person).x() * Debug.GRID,
                    context.loc(person).y() * Debug.GRID
                    + 1, Debug.GRID - 1,
                    Debug.GRID - 1);
          }
        });
        try {
          Main.debug.println("Whom would you like to heal?");
          target_loc = ui.await_location(context);
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
          throw new Game.QuitException("Quitting the game.", e);
        } finally {
          ui.pop();
        }

        if (target_loc == null) // user canceled
          return false;

        Combatant target = temp_context.at(target_loc);
        if (target == null || target.equals(c)
            || !c.staff().usable(temp_context, c, target))
          continue;

        Action.staff(c, dest, target).perform(context);
        return true;
      }
    }
  },
  TRADE("Trade") {
    public boolean usable(Battle context, Combatant c, MapLocation dest) {
      for (Combatant friend : context.roster(context.team(c)))
        if (dest.distance_to(context.loc(friend)) == 1
        && !friend.equals(c))
          return true;
      return false;
    }

    public boolean use(final Battle context, final UI.Manager ui,
        final Combatant c, final MapLocation dest) {
      final Battle temp_context = context.clone();
      temp_context.move(c, dest);
      MapLocation target_loc;
      try {
        class BooleanPointer {
          public boolean referent = false;
        }
        final BooleanPointer confirmed = new BooleanPointer();
        for (;;) {
          ui.push(new UI.Decorator(ui) {
            public void paint(TGraphics tg) {
              super.paint(tg);
              Graphics g = tg.get();
              g.setColor(new Color(0, 255, 0, 128));
              for (Combatant friend : context.roster(context
                  .team(c)))
                if (dest.distance_to(context.loc(friend)) == 1
                && !friend.equals(c))
                  g.fillRect(context.loc(friend).x()
                      * Debug.GRID, context.loc(friend)
                      .y() * Debug.GRID + 1,
                      Debug.GRID - 1, Debug.GRID - 1);
            }
          });
          try {
            Main.debug
            .println("With whom would you like to trade?");
            target_loc = ui.await_location(context);
          } finally {
            ui.pop();
          }

          if (target_loc == null) // user canceled
            return false;

          if (dest.distance_to(target_loc) == 1) {
            final Combatant target = context.at(target_loc);
            if (target != null && !target.equals(c)
                && context.team(target) == context.team(c)) {
              // debug
              {
                Main.log.print(c.name() + "'s items: ");
                for (Item i : c.inventory())
                  if (i != null)
                    Main.log.print(i + ". ");
                Main.log.println();
                Main.log.print(target.name() + "'s items: ");
                for (Item i : target.inventory())
                  if (i != null)
                    Main.log.print(i + ". ");
                Main.log.println();
              }

              class Helper {
                public boolean invoke()
                    throws InterruptedException {
                  for (;;) {
                    String[] give_take_choices = { "Give",
                    "Take" };
                    Integer give_take_selection;
                    give_take_selection = ui.await_choice(
                        "What would you like to do?",
                        give_take_choices,
                        Debug.menu_x(context),
                        Debug.menu_y(context));
                    if (give_take_selection == null) // user
                      // canceled
                      return false;
                    Combatant giver, taker;
                    if (give_take_selection == 0) { // give
                      giver = c;
                      taker = target;
                    } else { // take
                      giver = target;
                      taker = c;
                    }
                    String[] giver_item_choices = new String[Game.MAX_INVENTORY_SIZE];
                    for (int i = 0; i < giver_item_choices.length; ++i)
                      if (i < giver.inventory().size())
                        giver_item_choices[i] = giver
                        .inventory().get(i)
                        .name();
                      else
                        giver_item_choices[i] = " -- ";
                    for (;;) {
                      Integer giver_item_index = ui
                          .await_choice(
                              "Which item from "
                                  + giver.name()
                                  + "'s inventory?",
                                  giver_item_choices,
                                  Debug.menu_x(context),
                                  Debug.menu_y(context));
                      if (giver_item_index == null) // user
                        // canceled
                        break;
                      String[] taker_item_choices = new String[Game.MAX_INVENTORY_SIZE];
                      for (int i = 0; i < taker_item_choices.length; ++i)
                        if (i < taker.inventory()
                            .size())
                          taker_item_choices[i] = taker
                          .inventory().get(i)
                          .name();
                        else
                          taker_item_choices[i] = " -- ";
                      Integer taker_item_index = ui
                          .await_choice(
                              "To which slot in "
                                  + taker.name()
                                  + "'s inventory?",
                                  taker_item_choices,
                                  Debug.menu_x(context),
                                  Debug.menu_y(context));
                      if (taker_item_index == null) // user
                        // canceled
                        continue;
                      giver_item_index = Math.min(giver
                          .inventory().size(),
                          giver_item_index);
                      taker_item_index = Math.min(taker
                          .inventory().size(),
                          taker_item_index);
                      ListIterator<Item> giver_item_itor = giver
                          .inventory().listIterator(
                              giver_item_index);
                      ListIterator<Item> taker_item_itor = taker
                          .inventory().listIterator(
                              taker_item_index);
                      Item giver_item = null;
                      Item taker_item = null;
                      if (giver_item_itor.hasNext()) {
                        giver_item = giver_item_itor
                            .next();
                        giver_item_itor.remove();
                      }
                      if (taker_item_itor.hasNext()) {
                        taker_item = taker_item_itor
                            .next();
                        taker_item_itor.remove();
                      }
                      if (giver_item != null)
                        taker_item_itor.add(giver_item);
                      if (taker_item != null)
                        giver_item_itor.add(taker_item);
                      confirmed.referent = true;
                      return true;
                    }
                  }
                }
              }

              for (;;)
                if (!(new Helper().invoke()))
                  break;
              if (confirmed.referent) {
                context.move(c, dest);
                context.mark_gray(c);
                confirmed.referent = true;
                return true;
              }
            }
          }
        }
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new Game.QuitException("Quitting the game.", e);
      }
      // return false; //debug
    }
  },
  WAIT("Wait") {
    public boolean usable(Battle context, Combatant c, MapLocation dest) {
      return true;
    }

    public boolean use(Battle context, UI.Manager ui, Combatant c,
        MapLocation dest) {
      Action.wait(c, dest).perform(context);
      return true;
    }
  };

  // public methods
  public String getName() {
    return name_;
  }

  public abstract boolean usable(Battle context, Combatant c, MapLocation dest);

  // returns true if the game should return to unit-select mode (i.e. command
  // succeeded)
  // returns false if the game should return to the command menu (i.e. user
  // canceled)
  public abstract boolean use(Battle context, UI.Manager ui, Combatant c,
      MapLocation dest);

  // private ctor
  private Command(String name) {
    name_ = name;
  }

  // fields
  private String name_;
}

// //////////////////////////////////////////////////////////////

class Attributes {
  public Attributes(int max_hp, int str_mag, int skill, int speed, int luck,
      int defense, int resistance, int constitution) {
    this.max_hp = max_hp;
    this.str_mag = str_mag;
    this.skill = skill;
    this.speed = speed;
    this.luck = luck;
    this.defense = defense;
    this.resistance = resistance;
    this.constitution = constitution;
  }

  public int max_hp = 0;
  public int str_mag = 0;
  public int skill = 0;
  public int speed = 0;
  public int luck = 0;
  public int defense = 0;
  public int resistance = 0;
  public int constitution = 0;
}

// //////////////////////////////////////////////////////////////

enum WType {
  // enumerated list
  SWORD(0), AXE(1), LANCE(2), BOW(-1), ANIMA(0), LIGHT(1), DARK(2), STAFF(-1);

  public boolean is_magical() {
    return ordinal() >= 4;
  }

  public int triangle_bonus(WType other) {
    if (index_ == -1 || is_magical() != other.is_magical())
      return 0;
    return (4 + other.index_ - index_) % 3 - 1;
  }

  private WType(int triangle_index) {
    index_ = triangle_index;
  }

  private int index_;
}

// //////////////////////////////////////////////////////////////

enum WLvl {
  // enumerated list
  NONE, E, D, C, B, A, S;

  public boolean is_at_least(WLvl x) {
    return compareTo(x) >= 0;
  }
}

// //////////////////////////////////////////////////////////////

enum Mount {
  // enumerated list
  NONE, HORSE, NOMADIC_HORSE, PEGASUS, WYVERN;
}

// //////////////////////////////////////////////////////////////

enum CClass {
  // enumerated list
  HERO("Hero", Mount.NONE, 6, 0, null), MERCENARY("Mercenary", Mount.NONE, 5,
      0, HERO), SWORDMASTER("Swordmaster", Mount.NONE, 6, 15, null), MYRMIDON(
          "Myrmidon", Mount.NONE, 5, 0, SWORDMASTER), ASSASSIN("Assassin",
              Mount.NONE, 6, 0, null), THIEF("Thief", Mount.NONE, 6, 0, ASSASSIN), GENERAL(
                  "General", Mount.NONE, 5, 0, null), KNIGHT("Knight", Mount.NONE, 4,
                      0, GENERAL), SOLDIER("Soldier", Mount.NONE, 5, 0, null), WARRIOR(
                          "Warrior", Mount.NONE, 6, 0, null), FIGHTER("Fighter", Mount.NONE,
                              5, 0, WARRIOR), BRIGAND("Brigand", Mount.NONE, 5, 0, null), BERSERKER(
                                  "Berserker", Mount.NONE, 6, 15, null), PIRATE("Pirate", Mount.NONE,
                                      5, 0, BERSERKER), SNIPER("Sniper", Mount.NONE, 6, 0, null), ARCHER(
                                          "Archer", Mount.NONE, 5, 0, SNIPER), NOMAD_TROOPER("Nomad Trooper",
                                              Mount.NOMADIC_HORSE, 8, 0, null), NOMAD("Nomad",
                                                  Mount.NOMADIC_HORSE, 7, 0, NOMAD_TROOPER), PALADIN("Paladin",
                                                      Mount.HORSE, 8, 0, null), CAVALIER("Cavalier", Mount.HORSE, 7, 0,
                                                          PALADIN), FALCON_KNIGHT("Falcon Knight", Mount.PEGASUS, 8, 0, null), PEGASUS_KNIGHT(
                                                              "Pegasus Knight", Mount.PEGASUS, 7, 0, FALCON_KNIGHT), WYVERN_LORD(
                                                                  "Wyvern Lord", Mount.WYVERN, 8, 0, null), WYVERN_RIDER(
                                                                      "Wyvern Knight", Mount.WYVERN, 7, 0, WYVERN_LORD), BISHOP("Bishop",
                                                                          Mount.NONE, 6, 0, null), MONK("Monk", Mount.NONE, 5, 0, BISHOP), CLERIC(
                                                                              "Cleric", Mount.NONE, 5, 0, BISHOP), VALKYRIE("Valkyrie",
                                                                                  Mount.HORSE, 8, 0, null), TROUBADOUR("Troubadour", Mount.HORSE, 7,
                                                                                      0, VALKYRIE), SAGE("Sage", Mount.NONE, 6, 0, null), MAGE("Mage",
                                                                                          Mount.NONE, 5, 0, SAGE), DRUID("Druid", Mount.NONE, 6, 0, null), SHAMAN(
                                                                                              "Shaman", Mount.NONE, 5, 0, DRUID), BARD("Bard", Mount.NONE, 5, 0,
                                                                                                  null), DANCER("Dancer", Mount.NONE, 5, 0, null);

  // private ctor
  private CClass(String name, Mount mount, int movement_speed,
      int critical_bonus, CClass promotion) {
    name_ = name;
    mount_ = mount;
    movement_ = movement_speed;
    critical_ = critical_bonus;
    promotion_ = promotion;
  }

  // accessors
  public String getName() {
    return name_;
  }

  public int movement() {
    return movement_;
  }

  public Mount mount() {
    return mount_;
  }

  public int move_cost(Terrain t) { // debug stub
    switch (mount_) {
    case WYVERN: // fallthrough
    case PEGASUS:
      return 1;
      // implicit break
    case HORSE:
      return t == Terrain.FOREST ? 3 : t == Terrain.WALL ? 99 : 1;
    default:
      return t == Terrain.FOREST ? 2 : t == Terrain.WALL ? 99 : 1;
    }
  }

  public int critical() {
    return critical_;
  }

  public CClass promotion() {
    return promotion_;
  }

  // public methods
  public WeaponTemplate default_weapon() { // debug method
    switch (this) {
    case MERCENARY: // fallthrough
    case HERO: // fallthrough
    case MYRMIDON: // fallthrough
    case SWORDMASTER: // fallthrough
    case THIEF: // fallthrough
    case ASSASSIN:
      return WeaponTemplate.IRON_SWORD; // implicit break
    case KNIGHT: // fallthrough
    case GENERAL: // fallthrough
    case SOLDIER: // fallthrough
    case CAVALIER: // fallthrough
    case PALADIN: // fallthrough
    case PEGASUS_KNIGHT: // fallthrough
    case FALCON_KNIGHT: // fallthrough
    case WYVERN_RIDER: // fallthrough
    case WYVERN_LORD:
      return WeaponTemplate.IRON_LANCE; // implicit break
    case FIGHTER: // fallthrough
    case WARRIOR: // fallthrough
    case BRIGAND: // fallthrough
    case PIRATE: // fallthrough
    case BERSERKER:
      return WeaponTemplate.IRON_AXE; // implicit break
    case ARCHER: // fallthrough
    case SNIPER: // fallthrough
    case NOMAD: // fallthrough
    case NOMAD_TROOPER:
      return WeaponTemplate.IRON_BOW; // implicit break
    case MONK: // fallthrough
    case BISHOP:
      return WeaponTemplate.LIGHTNING; // implicit break
    case CLERIC: // fallthrough
    case DANCER: // fallthrough
    case BARD: // fallthrough
    case TROUBADOUR:
      return null; // implicit break
    case VALKYRIE: // fallthrough
    case MAGE: // fallthrough
    case SAGE:
      return WeaponTemplate.FIRE; // implicit break
    case SHAMAN: // fallthrough
    case DRUID:
      return WeaponTemplate.FLUX; // implicit break
    default:
      throw new Error("Assertion failed!");
    }
  }

  public StaffTemplate default_staff() { // debug method
    switch (this) {
    case BISHOP: // fallthrough
    case CLERIC: // fallthrough
    case TROUBADOUR: // fallthrough
    case VALKYRIE: // fallthrough
    case SAGE: // fallthrough
    case DRUID:
      return StaffTemplate.HEAL; // implicit break
    default:
      return null;
    }
  }

  // fields
  private String name_;
  private Mount mount_;
  private int movement_;
  private int critical_;
  private CClass promotion_;
}

// //////////////////////////////////////////////////////////////

enum Terrain {
  NULL("--", 0, 0), PLAINS("Plains", 0, 0), FOREST("Forest", 20, 1), WALL(
      "Wall", 0, 0);

  // accessors
  public String getName() {
    return name_;
  }

  public int evade() {
    return evade_;
  }

  public int defense() {
    return defense_;
  }

  // constant methods
  public void draw(Graphics g, int x, int y) { // debug stub
    switch (this) {
    case NULL:
      g.setColor(Color.WHITE);
      break;
    case PLAINS:
      g.setColor(new Color(192, 224, 128));
      break;
    case FOREST:
      g.setColor(new Color(64, 160, 128));
      break;
    case WALL:
      g.setColor(new Color(160, 160, 192));
      break;
    default:
      throw new Error("Assertion failed!"); // implicit break
    }
    g.fillRect(x * Debug.GRID, y * Debug.GRID, Debug.GRID, Debug.GRID);
  }

  // private ctor
  private Terrain(String name, int evade, int defense) {
    name_ = name;
    evade_ = evade;
    defense_ = defense;
  }

  // fields
  private String name_;
  private int evade_;
  private int defense_;
}

// //////////////////////////////////////////////////////////////

class MapLocation {
  // ctors
  public MapLocation() {
    x_ = -1;
    y_ = -1;
    terrain_ = Terrain.NULL;
  }

  public MapLocation(int x, int y, Terrain t) {
    if (x < 0 || y < 0)
      throw new Error("Assertion failed!");
    x_ = x;
    y_ = y;
    terrain_ = t;
  }

  // accessors
  public int x() {
    return x_;
  }

  public int y() {
    return y_;
  }

  public Terrain terrain() {
    return terrain_;
  }

  // constant methods
  public String toString() {
    return "(" + x_ + "," + y_ + ")";
  }

  public boolean is_null() {
    return x_ == -1;
  }

  public int distance_to(MapLocation that) {
    if (x_ == -1 || that.x_ == -1)
      return Integer.MAX_VALUE;
    else
      return Math.abs(x_ - that.x_) + Math.abs(y_ - that.y_);
  }

  // fields
  private int x_;
  private int y_;
  private Terrain terrain_; // debug stub
}

// //////////////////////////////////////////////////////////////

class Combatant implements Cloneable {
  public Combatant(String name, CClass cclass, Attributes attr,
      WeaponTemplate template) {
    name_ = name;
    cclass_ = cclass;
    attr_ = attr;
    hp_ = attr.max_hp;
    for (WType wt : WType.values())
      wlvl_map_.put(wt, WLvl.NONE);
    inventory_ = new ArrayList<Item>();

    id_ = new Object();

    // debug
    if (template != null) {
      wlvl_map_.put(template.type(), WLvl.A);
      inventory_.add(new Weapon(template));
    }
    if (cclass.default_staff() != null)
      wlvl_map_.put(WType.STAFF, WLvl.A);
  }

  // accessors
  public String name() {
    return name_;
  }

  public int hp() {
    return hp_;
  }

  public WLvl wlvl(WType wt) {
    return wlvl_map_.get(wt);
  }

  public CClass cclass() {
    return cclass_;
  }

  public List<Item> inventory() {
    return inventory_;
  }

  public int max_hp() {
    return attr_.max_hp;
  }

  public int magic() {
    return attr_.str_mag;
  }
  
  public int defense(){
	  return attr_.defense;
  }

  // constant methods
  public boolean equals(Object x) {
    Combatant c = (Combatant) x;
    return this.id_ == c.id_;
  }

  public int hashCode() {
    return id_.hashCode();
  }

  public Combatant clone() {
    try {
      return (Combatant) super.clone();
    } catch (CloneNotSupportedException e) {
      throw new Error("Assertion failed!", e);
    }
  }

  public boolean can_attack(Battle context, Combatant target) {
    if (!context.team(target).is_enemy_of(context.team(this)))
      return false;
    return weapon().within_range(context.loc(this), context.loc(target));
  }

  public boolean can_use_staff(Battle context, Combatant target) {
    for (Item item : inventory_) {
      if (item instanceof Staff) {
        Staff staff = (Staff) item;
        if (staff.can_be_used_by(this)
            && staff.usable(context, this, target))
          return true;
      }
    }
    return false;
  }

  public String toString() {
    return name_ + ", hp = " + hp_ + ", " + cclass_.getName() + "\n"
        + weapon().name() + "\n" + "Power: " + attr_.str_mag + "\n"
        + "Skill: " + attr_.skill + "\n" + "Speed: " + attr_.speed
        + "\n" + "Luck: " + attr_.luck + "\n" + "Defense: "
        + attr_.defense + "\n" + "Resist: " + attr_.resistance + "\n"
        + "Constitution: " + attr_.constitution + "\n";
  }

  public boolean dead() {
    return hp_ <= 0;
  }

  public void draw(UI.TGraphics tg, int x, int y, ColorMapping cm) {
    Graphics g = tg.get();
    tg.draw_image(cclass_.name() + ".png", x * Debug.GRID, y * Debug.GRID
        - Debug.GRID / 2, Debug.GRID, Debug.GRID * 3 / 2, cm);
  }

  public void draw_hp_bar(UI.TGraphics tg, int x, int y, Color color) {
    Graphics g = tg.get();
    g.setColor(Color.BLACK);
    g.fillRect(x * Debug.GRID, y * Debug.GRID + Debug.GRID * 7 / 8,
        Debug.GRID - 1, Debug.GRID - Debug.GRID * 7 / 8);
    g.setColor(color);
    g.fillRect(x * Debug.GRID, y * Debug.GRID + Debug.GRID * 7 / 8,
        (Debug.GRID - 1) * hp_ / attr_.max_hp, Debug.GRID - Debug.GRID
        * 7 / 8);
  }

  public void draw_info(Graphics g, int x, int y) {
    g.setColor(Color.BLACK);
    Util.linewrap_text(g, toString(), x, y);
  }

  public int movement() {
    return cclass_.movement();
  } // debug stub

  public Weapon weapon() {
    for (Item i : inventory_) {
      if (i instanceof Weapon) {
        Weapon w = (Weapon) i;
        if (w.can_be_used_by(this))
          return w;
      }
    }
    return Weapon.NULL;
  }

  public Staff staff() {
    for (Item i : inventory_) {
      if (i instanceof Staff) {
        Staff staff = (Staff) i;
        if (staff.can_be_used_by(this))
          return staff;
      }
    }
    return null;
  }

  public int attack_speed() {
    return attr_.speed
        - Math.max(0, weapon().weight() - attr_.constitution);
  }

  public int hit(Battle context) {
    return weapon().accuracy() + attr_.skill * 2 + attr_.luck / 2
        + support(context).accuracy()
        + (wlvl(weapon().type()) == WLvl.S ? 5 : 0);
  }

  public int evade(Battle context) {
    return attack_speed() * 2 + attr_.luck
        + context.loc(this).terrain().evade()
        + support(context).evade();
  }

  public int critical_rate(Battle context) {
    return weapon().critical() + attr_.skill / 2
        + support(context).critical() + cclass_.critical()
        + (wlvl(weapon().type()) == WLvl.S ? 5 : 0);
  }

  public int critical_evade(Battle context) {
    return attr_.luck + support(context).critical_evade();
  }

  public void draw_combat_info(Graphics g, int x, int y, Battle context,
      Combatant target) {
    CombatInfo my_info = new CombatInfo(context, this, target);
    CombatInfo enemy_info = new CombatInfo(context, target, this);
    int my_hit = my_info.hit();
    int my_damage = my_info.damage();
    int my_critical = my_info.critical();
    String my_multiplicity = my_info.multiplicity_string();

    int enemy_hit = enemy_info.hit();
    int enemy_damage = enemy_info.damage();
    int enemy_critical = enemy_info.critical();
    String enemy_multiplicity = enemy_info.multiplicity_string();

    String info = target.name() + ", hp = " + target.hp_ + ", "
        + target.cclass_.getName() + "\n" + target.weapon().name()
        + "\n";
    Util.linewrap_text(g, info, x, y);
    info = "\n\n\nHP\nDmg\nHit\nCrit";
    Util.linewrap_text(g, info, x + 60, y);
    info = "\n\nMe\n" + hp_ + "\n" + my_damage + my_multiplicity + "\n"
        + my_hit + "\n" + my_critical;
    Util.linewrap_text(g, info, x + 20, y);
    info = "\n\nEnemy\n" + target.hp_ + "\n" + enemy_damage
        + enemy_multiplicity + "\n" + enemy_hit + "\n" + enemy_critical;
    Util.linewrap_text(g, info, x + 100, y);
  }

  // mutators
  public void add_to_hp(int amount) {
    hp_ = Math.min(attr_.max_hp, hp_ + amount);
  }

  public void attack_miss(Battle context, Combatant target) {
    // nothing happens if we miss
  }

  public void attack_hit(Battle context, Combatant target) {
    CombatInfo info = new CombatInfo(context, this, target);
    int damage = info.damage();
    target.hp_ -= damage;
    if (target.dead())
      context.remove(target);
  }

  public void attack_critical(Battle context, Combatant target) {
    CombatInfo info = new CombatInfo(context, this, target);
    int damage = info.damage();
    target.hp_ -= 3 * damage;
    if (target.dead())
      context.remove(target);
  }

  public void attack(Battle context, Combatant target) {
    if (this.dead() || target.dead())
      return;

    Main.log.println(name() + " attacks " + target.name() + " (hp="
        + target.hp_ + ") with " + weapon().name());

    CombatInfo info = new CombatInfo(context, this, target);
    int hit = info.hit();
    int damage = info.damage();
    int critical = info.critical();

    // Main.log.println("  Hit: "+hit+", Damage: "+damage);
    double die_1 = Math.random();
    double die_2 = Math.random();
    int dice = (int) ((die_1 + die_2) * 50.0);
    if (dice < hit) {
      double raw_crit_die = Math.random();
      int crit_die = (int) (raw_crit_die * 100.0);
      if (crit_die < critical) {
        // Main.log.println("  CRIT! (("+die_1+" + "+die_2+") * 50.0 = "+dice+" and crit die = "+crit_die+")");
        attack_critical(context, target);
      } else {
        // Main.log.println("  Hit! (("+die_1+" + "+die_2+") * 50.0 = "+dice+")");
        attack_hit(context, target);
      }
    } else {
      // Main.log.println("  Miss! (("+die_1+" + "+die_2+") * 50.0 = "+dice+")");
      attack_miss(context, target);
    }
    // Main.log.println("  "+target.name()+"'s new hp: "+target.hp_);
  }

  public void acquire(Item item) {
    inventory_.add(item);
    if (inventory_.size() > Game.MAX_INVENTORY_SIZE)
      throw new Error("Stub code reached!"); // debug stub
  }

  // private methods
  private SupportBonus support(Battle context) {
    return new SupportBonus();
  } // debug stub

  public static class CombatInfo {
    public CombatInfo(Battle context, Combatant source, Combatant target) {
    	if (context == null) System.out.println("BATTLE IS NULL!!!"); //debug
    	if (source == null) System.out.println("SOURCE IS NULL!!!"); //debug
    	if (target == null) System.out.println("TARGET IS NULL!!!"); //debug
      if (!source.weapon().within_range(context.loc(source),
          context.loc(target))) {
        hit_ = critical_ = damage_ = 0;
        multiplicity_ = 0;
        return;
      }

      hit_ = source.hit(context)
          + Util.triangle_bonus_accuracy(source.weapon(),
              target.weapon()) - target.evade(context);
      hit_ = Math.max(0, Math.min(100, hit_));

      int defense = source.weapon().is_magical() ? target.attr_.resistance
          : target.attr_.defense;

      damage_ = (source.weapon().might() + Util.triangle_bonus_damage(
          source.weapon(), target.weapon()))
          * (source.weapon().effective(target) ? 3 : 1)
          + source.attr_.str_mag
          + source.support(context).damage()
          - context.loc(target).terrain().defense()
          - defense
          - target.support(context).defense();
      damage_ = Math.max(0, damage_);

      critical_ = source.critical_rate(context)
          - target.critical_evade(context);
      critical_ = Math.max(0, Math.min(100, critical_));

      // debug todo: make "x4" a possible value for multiplicity
      multiplicity_ = source.attack_speed() >= target.attack_speed() + 4 ? 2
          : 1;
    }

    // public methods
    public double expected_damage() {
      return hit() / 100.0 * damage()
          * ((Game.CRITICAL_MULTIPLIER - 1) * critical() / 100.0 + 1)
          * multiplicity_;
    }

    public String toString() {
      return "{" + hit_ + "%, " + damage_ + multiplicity_string() + " ("
          + critical_ + "% crit)}";
    }

    // accessors
    public int hit() {
      return hit_;
    }

    public int damage() {
      return damage_;
    }

    public int critical() {
      return critical_;
    }

    // debug todo: make "x4" a possible value for multiplicity
    public String multiplicity_string() {
      return multiplicity_ == 2 ? "x2" : "";
    } // double attack?

    public int multiplicity() {
      return multiplicity_;
    }

    // fields
    private int hit_;
    private int damage_;
    private int critical_;
    private int multiplicity_;
  }

  // fields
  private String name_;
  private CClass cclass_;
  private Attributes attr_;
  private int hp_;
  private Map<WType, WLvl> wlvl_map_ = new EnumMap<WType, WLvl>(WType.class);
  private List<Item> inventory_;
  private Object id_;
}

// //////////////////////////////////////////////////////////////

interface Item {
  public String name();
}

// //////////////////////////////////////////////////////////////

enum StaffTemplate {
  // enumerated list
  HEAL("Heal", WLvl.E) {
    public boolean usable(Battle context, Combatant source, Combatant target) {
      return !context.team(target).is_enemy_of(context.team(source))
          && target.hp() < target.max_hp()
          && context.loc(source).distance_to(context.loc(target)) == 1;
    }

    public void use(Battle context, Combatant source, Combatant target) {
      // System.out.println("    Healing "+target.name()); //debug
      if (context.at(context.loc(target)) != target) // hack ...
        throw new Error("Assertion failed!");
      target.add_to_hp(source.magic() + 10);
    }
  };

  // public methods
  public String getName() {
    return name_;
  }

  public abstract boolean usable(Battle context, Combatant source,
      Combatant target);

  public abstract void use(Battle context, Combatant source, Combatant target);

  public boolean can_be_used_by(Combatant c) {
    return c.wlvl(WType.STAFF).is_at_least(rank_);
  }

  // private ctor
  private StaffTemplate(String name, WLvl rank) {
    name_ = name;
    rank_ = rank;
  }

  // fields
  private String name_;
  private WLvl rank_;
}

// //////////////////////////////////////////////////////////////

class Staff implements Item {
  // ctor
  public Staff(StaffTemplate template) {
    template_ = template;
  }

  // delegation to StaffTemplate
  public String toString() {
    return name();
  }

  public String name() {
    return template_.getName();
  }

  public boolean usable(Battle context, Combatant source, Combatant target) {
    return template_.usable(context, source, target);
  }

  public boolean can_be_used_by(Combatant c) {
    return template_.can_be_used_by(c);
  }

  public void use(Battle context, Combatant source, Combatant target) {
    template_.use(context, source, target);
  }

  // fields
  private StaffTemplate template_;
}

// //////////////////////////////////////////////////////////////

class SupportBonus {
  public int evade() {
    return 0;
  } // debug stub

  public int damage() {
    return 0;
  } // debug stub

  public int defense() {
    return 0;
  } // debug stub

  public int accuracy() {
    return 0;
  } // debug stub

  public int critical() {
    return 0;
  } // debug stub

  public int critical_evade() {
    return 0;
  } // debug stub
}

// //////////////////////////////////////////////////////////////

enum WeaponTemplate {
  // enumerated list
  IRON_SWORD("Iron Sword", WType.SWORD, WLvl.E, 1, 1, 5, 90, 0, 5, 46, 10), SLIM_SWORD(
      "Slim Sword", WType.SWORD, WLvl.E, 1, 1, 3, 100, 5, 2, 30, 16), ARMORSLAYER(
          "Armorslayer", WType.SWORD, WLvl.D, 1, 1, 8, 80, 0, 11, 18, 70) {
    public boolean effective(Combatant target) {
      return target.cclass() == CClass.KNIGHT
          || target.cclass() == CClass.GENERAL;
    }
  },
  IRON_AXE("Iron Axe", WType.AXE, WLvl.E, 1, 1, 8, 75, 0, 10, 45, 6), IRON_LANCE(
      "Iron Lance", WType.LANCE, WLvl.E, 1, 1, 7, 80, 0, 8, 45, 8), KILLER_BOW(
          "Killer Bow", WType.BOW, WLvl.C, 2, 2, 9, 75, 30, 7, 20, 70), IRON_BOW(
              "Iron Bow", WType.BOW, WLvl.E, 2, 2, 6, 85, 0, 5, 45, 12), FIRE(
                  "Fire", WType.ANIMA, WLvl.E, 1, 2, 5, 90, 0, 4, 40, 14), LIGHTNING(
                      "Lightning", WType.LIGHT, WLvl.E, 1, 2, 4, 95, 5, 6, 35, 18), FLUX(
                          "Flux", WType.DARK, WLvl.E, 1, 2, 7, 80, 0, 8, 45, 20);

  // public methods
  public boolean effective(Combatant target) {
    if (type_ == WType.BOW) {
      switch (target.cclass().mount()) {
      case WYVERN: // fallthrough
      case PEGASUS:
        return true; // implicit break
      }
    }
    return false;
  }

  public boolean within_range(MapLocation source_loc, MapLocation target_loc) {
    return within_range(source_loc.distance_to(target_loc));
  }

  public boolean within_range(int distance) {
    return distance >= min_range_ && distance <= max_range_;
  }

  public boolean is_magical() {
    return type_.is_magical();
  }

  public boolean can_be_used_by(Combatant c) {
    return c.wlvl(type_).is_at_least(rank_);
  }

  public int triangle_bonus(Weapon other) {
    if (other == Weapon.NULL)
      return 0;
    else
      return type().triangle_bonus(other.type());
  }

  // accessors
  public String getName() {
    return name_;
  }

  public WType type() {
    return type_;
  }

  public WLvl rank() {
    return rank_;
  }

  public int min_range() {
    return min_range_;
  }

  public int max_range() {
    return max_range_;
  }

  public int might() {
    return might_;
  }

  public int accuracy() {
    return accuracy_;
  }

  public int critical() {
    return critical_;
  }

  public int weight() {
    return weight_;
  }

  public int durability() {
    return durability_;
  }

  public int value() {
    return value_;
  }

  // private ctor
  private WeaponTemplate(String name, WType type, WLvl rank, int min_range,
      int max_range, int might, int accuracy, int critical, int weight,
      int durability, int value) {
    name_ = name;
    type_ = type;
    rank_ = rank;
    min_range_ = min_range;
    max_range_ = max_range;
    might_ = might;
    accuracy_ = accuracy;
    critical_ = critical;
    weight_ = weight;
    durability_ = durability;
    value_ = value;
  }

  // fields
  private String name_;
  private WType type_;
  private WLvl rank_;
  private int min_range_;
  private int max_range_;
  private int might_;
  private int accuracy_;
  private int critical_;
  private int weight_;
  private int durability_;
  private int value_; // gold per durability
}

// //////////////////////////////////////////////////////////////

class Weapon implements Item {
  // static constant
  public static final Weapon NULL = new Weapon(null) {
    public String name() {
      return "Unarmed";
    }

    public int might() {
      return 0;
    }

    public int accuracy() {
      return 0;
    }

    public int weight() {
      return 0;
    }

    public int critical() {
      return 0;
    }

    public WType type() {
      return null;
    }

    public WLvl rank() {
      return null;
    }

    public boolean is_magical() {
      return false;
    }

    public boolean effective(Combatant c) {
      return false;
    }

    public boolean within_range(MapLocation s, MapLocation t) {
      return false;
    }

    public boolean within_range(int d) {
      return false;
    }

    public boolean can_be_used_by(Combatant c) {
      return true;
    }

    public int triangle_bonus(Weapon w) {
      return 0;
    }
  };

  // ctor
  public Weapon(WeaponTemplate template) {
    template_ = template;
  }

  public String toString() {
    return name();
  }

  // delegation to WeaponTemplate
  public String name() {
    return template_.getName();
  }

  public int might() {
    return template_.might();
  }

  public int accuracy() {
    return template_.accuracy();
  }

  public int weight() {
    return template_.weight();
  }

  public int critical() {
    return template_.critical();
  }

  public WType type() {
    return template_.type();
  }

  public WLvl rank() {
    return template_.rank();
  }

  public boolean is_magical() {
    return template_.is_magical();
  }

  public boolean effective(Combatant target) {
    return template_.effective(target);
  }

  public boolean within_range(MapLocation source_loc, MapLocation target_loc) {
    return template_.within_range(source_loc, target_loc);
  }

  public boolean within_range(int distance) {
    return template_.within_range(distance);
  }

  public int min_range() {return template_.min_range();}
  public int max_range() {return template_.max_range();}

  public boolean can_be_used_by(Combatant c) {
    return template_.can_be_used_by(c);
  }

  public int triangle_bonus(Weapon other) {
    return template_.triangle_bonus(other);
  }

  // fields
  private WeaponTemplate template_;
}

// //////////////////////////////////////////////////////////////

class Util {
  private Util() {
    throw new UnsupportedOperationException();
  }

  static int triangle_bonus_damage(Weapon source, Weapon target) {
    return source.triangle_bonus(target) * 1;
  }

  static int triangle_bonus_accuracy(Weapon source, Weapon target) {
    return source.triangle_bonus(target) * 15;
  }

  static void draw_cursor(Graphics g, int x, int y) {
    g.setColor(Color.BLACK);
    g.drawRect(x + 1, y + 1, Debug.GRID, Debug.GRID);
    g.setColor(Color.WHITE);
    g.drawRect(x, y, Debug.GRID, Debug.GRID);
  }

  static void linewrap_text(Graphics g, String msg, int x, int y) {
    int i = 0;
    int begin = 0;
    int end = msg.indexOf('\n');
    if (end == -1)
      end = msg.length();
    while (begin < msg.length()) {
      g.drawString(msg.substring(begin, end), x, y + i * 20);
      begin = end;
      end = msg.indexOf('\n', end + 1);
      if (end == -1)
        end = msg.length();
      ++i;
    }
  }
}

// //////////////////////////////////////////////////////////////

enum Dir {
  // enumerated list
  WEST(0), EAST(1), NORTH(2), SOUTH(3);

  // private ctor
  private Dir(int k) {
    vert_ = (k / 2 == 1);
    pos_ = (k % 2 == 1);
  }

  // public methods
  int getH() {
    return vert_ ? 0 : pos_ ? 1 : -1;
  }

  int getV() {
    return !vert_ ? 0 : pos_ ? 1 : -1;
  }

  int getK() {
    return (vert_ ? 2 : 0) + (pos_ ? 1 : 0);
  }

  // statics
  static Dir valueOf(int k) {
    return values()[k];
  }

  static Dir valueOf(int x, int y) {
    return y == 0 ? (x < 0 ? WEST : EAST) : (y < 0 ? NORTH : SOUTH);
  }

  static Dir valueOf(boolean vert, boolean pos) {
    return vert ? (pos ? SOUTH : NORTH) : (pos ? EAST : WEST);
  }

  // fields
  private boolean vert_;
  private boolean pos_;
};

// //////////////////////////////////////////////////////////////

class Path {
  // accessors
  public Dir[] array() {
    return array_;
  }

  public int cost() {
    return cost_;
  }

  // public methods
  public int length() {
    return array_.length;
  }

  // ctor
  Path(Dir[] array, int cost) {
    array_ = array;
    cost_ = cost;
  }

  // fields
  private Dir[] array_;
  private int cost_;
};

// //////////////////////////////////////////////////////////////
